﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Windows.Forms;
using System.Data.SqlClient;
using System.Threading;
using System.IO;
/*
	Code borrowed and heavily modified from the ExpressProfiler codeplex 
	project http://expressprofiler.codeplex.com
	with the permission of the original author Locky.
*/

namespace SqlServer.Profiler
{
	// User control to display a trace log and Data window in splitter window.
	public partial class SqlProfileWindow : UserControl
	{
		// Profiler control configuration. 
		private bool scroll = false;

		/// <summary>
		/// Delegate for the Profiler state change event handler
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e">object containing the state that the profiler is currently in</param>
		public delegate void ProfileStateChangedEventHandler(object sender, ProfileStateChangeEventArgs e);

		/// <summary>
		/// Event handler for profiler change of state notification: Start, stop etc.
		/// Use to set the state of controls external to this window.
		/// </summary>
		public ProfileStateChangedEventHandler ProfileStateChanged = null;
		//public StreamWriter Logger = null;
		public String LogFile = null;

		/////////////////////////////////////////////////////////////////////////////////////////////
		// constructor
		public SqlProfileWindow()
		{
			InitializeComponent();
			InitLV();
		}

		/// <summary>
		/// Constructor with parameters for configuration
		/// </summary>
		/// <param name="connectionString">Database connection string for the SQL Server instance to profile</param>
		/// <param name="reportColumns">Array of report columns to display</param>
		/// <param name="events">Array of events to listen for</param>
		public SqlProfileWindow(String connectionString, ProfilerColumnConfiguration[] reportColumns, ProfilerEventsConfiguration[] events)
		{
			InitializeComponent();
			InitLV();
			SetConfiguration(connectionString, reportColumns, events);
		}

		/// <summary>
		/// Configure a profiler session
		/// </summary>
		/// <param name="connectionString">Database connection string for the SQL Server instance to profile</param>
		/// <param name="reportColumns">Array of report columns to display</param>
		/// <param name="events">Array of events to listen for</param>
		public void SetConfiguration(String connectionString, ProfilerColumnConfiguration[] reportColumns, ProfilerEventsConfiguration[] events)
		{
			// check whether this is a re-configuration or initial configuration
			if (!String.IsNullOrEmpty(connectionString))
				ConnectionString = connectionString;	// its the initial.
			this.reportColumns = reportColumns;
			this.eventList = events;
		}

		/////////////////////////////////////////////////////////////////////////////////////////////
		// control interface
		/// <summary>
		/// Start profiliing the server for the events and data previously passed in to the configuration method
		/// </summary>
		public void StartProfiling()
		{
			if (this.reportColumns == null || this.eventList == null)
				throw new NullReferenceException("Cannot start a profile until it has been configured with events and report columns");

			// add the list view sub-items
			eventsListView.Columns.Clear();

			// add the EventClass here.
			// AddReportColumn(column);
			foreach (ProfilerColumnConfiguration column in this.reportColumns)
			{
				AddReportColumn(column);
			}

			InternalStartProfiling(this.eventList, this.reportColumns);
			timerProfiler.Enabled = true;

			OnChangeState(ProfileStateChangeEventArgs.ProfileState.Start);
		}

		private void AddReportColumn(ProfilerColumnConfiguration column)
		{
			int width = (column.Id == ExpressProfiler.ProfilerEventColumns.TextData) ? 250 :
						(column.Id == ExpressProfiler.ProfilerEventColumns.EventClass) ? 120 :
						(column.DataType == "int") ? 50 :
						(column.DataType == "bigint") ? 75 : 100;
			HorizontalAlignment align = (column.DataType == "int" || column.DataType == "bigint") ? HorizontalAlignment.Right : HorizontalAlignment.Left;
			eventsListView.Columns.Add(column.Title, width).TextAlign = align;
		}

		/// <summary>
		/// Stop the current profiling session
		/// </summary>
		public void StopProfiling()
		{
			timerProfiler.Enabled = false;
			InternalStopProfiling();
			OnChangeState(ProfileStateChangeEventArgs.ProfileState.Stop);
		}

		/// <summary>
		/// Pause the current profiling session
		/// </summary>
		public void PauseProfiling()
		{
			timerProfiler.Enabled = false;
			InternalPauseProfiling();
			OnChangeState(ProfileStateChangeEventArgs.ProfileState.Pause);
		}

		/// <summary>
		/// Turn the profile report scrolling on or off. Normally set scrolling on to
		/// watch events as they happen. Turn off to examine interesting entries without them
		/// disappearing from the screen.
		/// </summary>
		/// <param name="scrollOn"></param>
		public void SetScrollTraceWindow(bool scrollOn)
		{
			this.scroll = scrollOn;
		}

		/// <summary>
		/// Clear the trace list window and data behind it.
		/// </summary>
		public void ClearTrace()
		{
			lock (eventsListView)
			{
				this.cached.Clear();
				eventsListView.VirtualListSize = 0;
				ListView1_ItemSelectionChanged_1(eventsListView, null);
				eventsListView.Invalidate();
			}
		}

		/////////////////////////////////////////////////////////////////////////////////////////////
		// configuration and set up
		private void InitLV()
		{
			eventsListView = new ExpressProfiler.ListViewNF
						   {
							   Dock = DockStyle.Fill,
							   Location = new System.Drawing.Point(0, 0),
							   Name = "lvEvents",
							   Size = new System.Drawing.Size(979, 297),
							   TabIndex = 0,
							   VirtualMode = true,
							   UseCompatibleStateImageBehavior = false,
							   BorderStyle = BorderStyle.None,
							   FullRowSelect = true,
							   View = View.Details,
							   GridLines = true,
							   HideSelection = false,
							   MultiSelect = false,
							   AllowColumnReorder = false
						   };
			eventsListView.RetrieveVirtualItem += LvEvents_RetrieveVirtualItem;
			eventsListView.KeyDown += LvEvents_KeyDown;
			eventsListView.ItemSelectionChanged += ListView1_ItemSelectionChanged_1;

			eventsListView.ColumnClick += LvEvents_ColumnClick;
			splitContainer1.Panel1.Controls.Add(eventsListView);
		}

		/////////////////////////////////////////////////////////////////////////////////////////////
		// Window events
		private void MainFormClosing(object sender, FormClosingEventArgs e)
		{
			if (this.profilingState == ProfilingStateEnum.psPaused || this.profilingState == ProfilingStateEnum.psProfiling)
			{
				StopProfiling();
			}
		}

		void LvEvents_ColumnClick(object sender, ColumnClickEventArgs e)
		{
		}

		private void ListView1_ItemSelectionChanged_1(object sender, ListViewItemSelectionChangedEventArgs e)
		{
			OnChangeListSelection();
		}

		private void LvEvents_KeyDown(object sender, KeyEventArgs e)
		{
			if (e.Control && e.Shift && e.KeyCode == Keys.Delete)
			{
				ClearTrace();
			}
		}

		private void OnChangeState(ProfileStateChangeEventArgs.ProfileState profileState)
		{
			if (ProfileStateChanged != null)
				ProfileStateChanged(this, new ProfileStateChangeEventArgs(profileState));
		}

		///////////////////////////////////////////////////////////////////////////////////////////////
		// Virtual list box callback
		private void LvEvents_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
		{
			e.Item = this.cached[e.ItemIndex];
		}

		///////////////////////////////////////////////////////////////////////////////////////////////
		// below here is code salvaged from express profiler
		// the list view, the threading, the start, stop, pause and resume logic.
		//

		// start
		private void InternalStartProfiling(ProfilerEventsConfiguration[] eventsWanted, ProfilerColumnConfiguration[] columns)
		{
			if (this.profilingState == ProfilingStateEnum.psPaused)
			{
				InternalResumeProfiling();
				return;
			}
			if (this.databaseConnection != null && this.databaseConnection.State == ConnectionState.Open)
			{
				this.databaseConnection.Close();
			}
			this.eventCount = 0;
			this.databaseConnection = GetConnection();
			this.databaseConnection.Open();
			this.traceReader = new ExpressProfiler.RawTraceReader(this.databaseConnection);

			this.traceReader.CreateTrace();

			// Register events to trace
			foreach (ProfilerEventsConfiguration eventConfig in eventsWanted)
			{
				this.traceReader.SetEvent(eventConfig.Id, ProfilerConfiguration.GetConnectedColumns(eventConfig, columns));
			}

			// Apply column data filters
			foreach (ProfilerColumnConfiguration column in this.reportColumns)
			{
			    ProfilerColumnConfiguration.SetFilter(this.traceReader, column);
			}

			this.sqlCommand.Connection = this.databaseConnection;
			this.sqlCommand.CommandTimeout = 0;
			this.cached.Clear();
			this.events.Clear();
			eventsListView.VirtualListSize = 0;
			StartProfilerThread();
		}

		private void StartProfilerThread()
		{
			if (this.traceReader != null)
			{
				this.traceReader.Close();
			}
			this.traceReader.StartTrace();
			this.theThread = new Thread(ProfilerThread) { IsBackground = true, Priority = ThreadPriority.Lowest };
			this.needStop = false;
			this.profilingState = ProfilingStateEnum.psProfiling;
			NewEventArrived(this.eventStarted, true);
			this.theThread.Start();
		}

		private void InternalStopProfiling()
		{
			using (SqlConnection cn = GetConnection())
			{
				cn.Open();
				this.traceReader.StopTrace(cn);
				this.traceReader.CloseTrace(cn);
				cn.Close();
			}
			this.needStop = true;
			if (this.theThread.IsAlive)
			{
				this.theThread.Abort();
			}
			this.theThread.Join();
			this.profilingState = ProfilingStateEnum.psStopped;
			NewEventArrived(this.eventStopped, true);
		}

		// pause
		private void InternalPauseProfiling()
		{
			using (SqlConnection cn = GetConnection())
			{
				cn.Open();
				this.traceReader.StopTrace(cn);
				cn.Close();
			}
			this.profilingState = ProfilingStateEnum.psPaused;
			NewEventArrived(this.eventPaused, true);
		}

		// resume
		private void InternalResumeProfiling()
		{
			StartProfilerThread();
		}

		///////////////////////////////////////////////////////////////////////////////
		// Periodic check of the state of the trace
		private void Timer1_Tick(object sender, EventArgs e)
		{
			Queue<ExpressProfiler.ProfilerEvent> saved;
			lock (this)
			{
				saved = this.events;
				this.events = new Queue<ExpressProfiler.ProfilerEvent>(EVENT_QUEUESIZE);
			}
			lock (this.cached)
			{
				while (0 != saved.Count)
				{
					NewEventArrived(saved.Dequeue(), 0 == saved.Count);
				}
				if (this.cached.Count > this.maxEvents)
				{
					while (this.cached.Count > this.maxEvents)
					{
						this.cached.RemoveAt(0);
					}
					eventsListView.VirtualListSize = this.cached.Count;
					eventsListView.Invalidate();
				}
			}
		}

		private void NewEventArrived(ExpressProfiler.ProfilerEvent evt, bool last)
		{
			{
				this.eventCount++;
				string caption;
				if (evt == this.eventStarted)
				{
					caption = "Trace started";
				}
				else
					if (evt == this.eventPaused)
					{
						caption = "Trace paused";
					}
					else
						if (evt == this.eventStopped)
						{
							caption = "Trace stopped";
						}
						else
						{
							caption = ExpressProfiler.ProfilerEvents.Names[evt.EventClass];
						}
				ListViewItem lvi = new ListViewItem(caption);

				String[] items = FormatEventData(evt, this.reportColumns);
				lvi.SubItems.AddRange(items);

				lvi.Tag = new ExpressProfiler.ProfilerEvent();
				this.cached.Add(lvi);
				if (last)
				{
					eventsListView.VirtualListSize = this.cached.Count;
					if (this.scroll)
					{
						eventsListView.Items[this.cached.Count - 1].Focused = true;
						eventsListView.Items[this.cached.Count - 1].Selected = true;
						ListView1_ItemSelectionChanged_1(eventsListView, null);
						eventsListView.EnsureVisible(this.cached.Count - 1);
					}

					eventsListView.Invalidate(lvi.Bounds);
				}

				LogEvent(LogFile, items, this.reportColumns);
			}
		}
		//
		private string[] FormatEventData(ExpressProfiler.ProfilerEvent evt, ProfilerColumnConfiguration[] reportColumns)
		{
			// 1st report column is already catered for.
			// Hence array sized to column count -1
			// and first report column ignored when populating the array
			String[] items = new String[this.reportColumns.GetLength(0) - 1];
			int idx = 0;
			bool firstColumn = true;
			foreach (ProfilerColumnConfiguration column in this.reportColumns)
			{
				if (firstColumn)
					firstColumn = false;
				else
				{
					items[idx++] = ProfilerColumnConfiguration.GetEventData(evt, column);
				}
			}
			return items;
		}

		private void LogEvent(String logFile, string[] items, ProfilerColumnConfiguration[] reportColumns)
		{
			// nothing to do.
			if (String.IsNullOrEmpty(logFile)) return;

			using (StreamWriter logger = new StreamWriter(logFile, true))
			{
				String sql = ""; // save til last
				int idx = 0;
				bool firstColumn = true;
				foreach (ProfilerColumnConfiguration column in this.reportColumns)
				{
					if (firstColumn)
						firstColumn = false;
					else
					{
						if (column.Title == "TextData")
							sql = items[idx];
						else
						{
							logger.WriteLine("{0}: {1}", column.Title, items[idx]);
						}
						++idx;
					}
				}
				if (!String.IsNullOrEmpty(sql))
				{
					logger.WriteLine("Data: \n{0}",sql); 
								}
				logger.WriteLine("%%%"); // old unix divider
				logger.Close();			 // possibly not needed, but demonstrates intention to close file every time.
			}
		}

		private void ProfilerThread(Object state)
		{
			while (!this.needStop && this.traceReader.TraceIsActive)
			{
				ExpressProfiler.ProfilerEvent evt = this.traceReader.Next();
				if (evt != null)
				{
					lock (this)
					{
						this.events.Enqueue(evt);
					}
				}
			}
		}

		// invoked when new lines are added or the user selects profiler line
		private void OnChangeListSelection()
		{
			// find text pane
			int textDataColumnIndex = -1;
			int idx = 0;
			foreach (ProfilerColumnConfiguration column in this.reportColumns)
			{
				if (column.Id == ExpressProfiler.ProfilerEventColumns.TextData)
					textDataColumnIndex = idx;
				else
					++idx;
			}
			if (textDataColumnIndex > -1)
				UpdateTextPane(eventsListView.FocusedItem, textDataColumnIndex);
		}

		private void UpdateTextPane(ListViewItem lvi, int textDataColumnIndex)
		{
			if (lvi == null || textDataColumnIndex == -1)
				reTextData.Text = "";
			else
				this.lexer.FillRichEdit(reTextData, lvi.SubItems[textDataColumnIndex].Text);
		}

		private SqlConnection GetConnection()
		{
			return new SqlConnection(ConnectionString);
		}

		// TODO: Refactor so applicatation sets the value
		String ConnectionString { get; set; }

		////////////////////////////////////////////////////////////////////////////////////////////
		// data required for the basic profiler list functions

		// SQL server profile/trace reader engine that this window wraps.
		private ExpressProfiler.RawTraceReader traceReader;
		// delegates for callbacks from RawTraceReader
		private readonly ExpressProfiler.ProfilerEvent eventStarted = new ExpressProfiler.ProfilerEvent();
		private readonly ExpressProfiler.ProfilerEvent eventStopped = new ExpressProfiler.ProfilerEvent();
		private readonly ExpressProfiler.ProfilerEvent eventPaused = new ExpressProfiler.ProfilerEvent();
		// state of the current trace run
		private enum ProfilingStateEnum { psStopped, psProfiling, psPaused }
		private ProfilingStateEnum profilingState;
		// thread managing the trace.
		private Thread theThread;
		private bool needStop = true;  // flag from UI to thread set to stop trace

		// the profiler list view
		private ExpressProfiler.ListViewNF eventsListView;
		// profiler list rows
		private readonly List<ListViewItem> cached = new List<ListViewItem>(1024);
		private int eventCount;
		private int maxEvents = 1000;
		const int EVENT_QUEUESIZE = 10; // don't know why. Inherited from original code.
		Queue<ExpressProfiler.ProfilerEvent> events = new Queue<ExpressProfiler.ProfilerEvent>(EVENT_QUEUESIZE);

		// The actual current configuration
		ProfilerColumnConfiguration[] reportColumns = null;
		ProfilerEventsConfiguration[] eventList = null;

		// Sql Pane
		private readonly ExpressProfiler.YukonLexer lexer = new ExpressProfiler.YukonLexer();

		// Database access
		private SqlConnection databaseConnection;
		private readonly SqlCommand sqlCommand = new SqlCommand();

		private void TextData_MouseDown(object sender, MouseEventArgs e)
		{
			if (e.Button == System.Windows.Forms.MouseButtons.Right)
			{
				// copy the SQL to the clipboard and give visual feedback
				RichTextBox tb = sender as RichTextBox;
				Clipboard.SetText(tb.Text);
				// feeddback
				// hold the current state
				String holdRtf = tb.Rtf;
				System.Drawing.Color hold = tb.BackColor;
				// clear text so whole panel flashes
				tb.Text = "";
				// make panel red and hold that state for 50ms.
				tb.BackColor = System.Drawing.Color.Red;
				tb.Refresh();
				Thread.Sleep(50);
				// put everything back to original state.
				tb.BackColor = hold;
				tb.Rtf = holdRtf;
				tb.Refresh();
			}
		}
	}

	// Class to pass the changed state of the profiler 
	public class ProfileStateChangeEventArgs : EventArgs
	{
		public enum ProfileState { Start, Stop, Pause, Resume };

		public ProfileStateChangeEventArgs(ProfileState state)
		{
			State = state;
		}

		public ProfileState State { get; set; }
	}
}
